﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Effects;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Windows.Navigation;
using System.Windows.Shapes;

using Drawing = System.Drawing;
using Imaging = System.Drawing.Imaging;
using Forms   = System.Windows.Forms;

namespace Cryomancer.StarChart
{
	/// <summary>
	/// Follow steps 1a or 1b and then 2 to use this custom control in a XAML file.
	///
	/// Step 1a) Using this custom control in a XAML file that exists in the current project.
	/// Add this XmlNamespace attribute to the root element of the markup file where it is 
	/// to be used:
	///
	///     xmlns:MyNamespace="clr-namespace:Cryomancer.StarChart"
	///
	///
	/// Step 1b) Using this custom control in a XAML file that exists in a different project.
	/// Add this XmlNamespace attribute to the root element of the markup file where it is 
	/// to be used:
	///
	///     xmlns:MyNamespace="clr-namespace:Cryomancer.StarChart;assembly=Cryomancer.StarChart"
	///
	/// You will also need to add a project reference from the project where the XAML file lives
	/// to this project and Rebuild to avoid compilation errors:
	///
	///     Right click on the target project in the Solution Explorer and
	///     "Add Reference"->"Projects"->[Browse to and select this project]
	///
	///
	/// Step 2)
	/// Go ahead and use your control in the XAML file.
	///
	///     <MyNamespace:VSPChartView/>
	///
	/// </summary>
	[TemplatePart(Name="Part_viewportMain",           Type=typeof(Viewport3D))]
	[TemplatePart(Name="Part_cameraMain",             Type=typeof(PerspectiveCamera))]
	[TemplatePart(Name="Part_modelMainPage",          Type=typeof(ModelVisual3D))]
	[TemplatePart(Name="Part_buttonFlip",             Type=typeof(Button))]
	[TemplatePart(Name="Part_geometryMainPage",       Type=typeof(GeometryModel3D))]
	[TemplatePart(Name="Part_imageBrushFront",        Type=typeof(ImageBrush))]
	[TemplatePart(Name="Part_imageBrushBack",         Type=typeof(ImageBrush))]
	[TemplatePart(Name="Part_blurEffectEnabledState", Type=typeof(BlurBitmapEffect))]
	public class VSPChartView : Control
	{
		#region Events

		#region ChartZoom RoutedEvent
		public static RoutedEvent ChartZoomEvent;
		public static RoutedEvent PreviewChartZoomEvent;

		public event ChartZoomRoutedEventHandler ChartZoom
		{
			add    { AddHandler(ChartZoomEvent, value);    }
			remove { RemoveHandler(ChartZoomEvent, value); }
		}

		public event ChartZoomRoutedEventHandler PreviewChartZoom
		{
			add    { AddHandler(PreviewChartZoomEvent, value);    }
			remove { RemoveHandler(PreviewChartZoomEvent, value); }
		}
		#endregion	

		#endregion

		#region Members
		private Point startDragPoint;
		private bool  isShowingFront;
		#endregion

		#region Properties
		public static readonly DependencyProperty CurrentChartProperty;
		public static readonly DependencyProperty ZoomFactorProperty;
		public static readonly DependencyProperty ZoomOutThresholdProperty;
		public static readonly DependencyProperty ZoomInThresholdProperty;
		public static readonly DependencyProperty EnabledProperty;

		/// <summary>
		/// Gets or sets the current chart displayed, if any.  Can be null.
		/// </summary>
		public VSPChartDetail CurrentChart
		{
			get
			{
				return (VSPChartDetail)GetValue(CurrentChartProperty);
			}
			set
			{
				SetValue(CurrentChartProperty, value);
			}
		}

		/// <summary>
		/// Gets or sets the multiplier for zooming
		/// </summary>
		public double ZoomFactor
		{
			get
			{
				return (double)GetValue(ZoomFactorProperty);
			}
			set
			{
				SetValue(ZoomFactorProperty, value);
			}
		}

		/// <summary>
		/// Gets or sets the threshold to load a wider FOV chart
		/// </summary>
		public double ZoomOutThreshold
		{
			get
			{
				return (double)GetValue(ZoomOutThresholdProperty);
			}
			set
			{
				SetValue(ZoomOutThresholdProperty, value);
			}
		}
		
		/// <summary>
		/// Gets or sets the point when a closer FOV chart is loaded
		/// </summary>
		public double ZoomInThreshold
		{
			get
			{
				return (double)GetValue(ZoomInThresholdProperty);
			}
			set
			{
				SetValue(ZoomInThresholdProperty, value);
			}
		}

		/// <summary>
		/// Gets or sets whether the chart is available for UI manipulation
		/// </summary>
		public bool Enabled
		{
			get
			{
				return (bool)GetValue(EnabledProperty);
			}
			set
			{
				SetValue(EnabledProperty, value);
			}
		}
		#endregion

		#region C'Tors and object lifetime code
		public VSPChartView()
		{
			this.startDragPoint = new Point();
			this.isShowingFront = true;
		}

		static VSPChartView()
		{
			ChartZoomEvent = EventManager.RegisterRoutedEvent(
				"ChartZoom",
				RoutingStrategy.Bubble,
				typeof(ChartZoomRoutedEventHandler),
				typeof(VSPChartView));

			PreviewChartZoomEvent = EventManager.RegisterRoutedEvent(
				"PreviewChartZoom",
				RoutingStrategy.Tunnel,
				typeof(ChartZoomRoutedEventHandler),
				typeof(VSPChartView));

			PropertyChangedCallback curChartPropChanged = new PropertyChangedCallback(OnCurrentChartChanged);
			PropertyMetadata curChartProp = new PropertyMetadata(null, curChartPropChanged);
			CurrentChartProperty = DependencyProperty.Register(
				"CurrentChart",
				typeof(VSPChartDetail),
				typeof(VSPChartView),
				curChartProp);

			PropertyChangedCallback propZoomFactorChanged = new PropertyChangedCallback(OnZoomFactorChanged);
			PropertyMetadata propZoomFactor = new PropertyMetadata(0.05d, propZoomFactorChanged);
			ZoomFactorProperty = DependencyProperty.Register(
				"ZoomFactor",
				typeof(double),
				typeof(VSPChartView),
				propZoomFactor);

			PropertyChangedCallback propZoomOutThresholdChanged = new PropertyChangedCallback(OnZoomOutThresholdChanged);
			PropertyMetadata propZoomOutThreshold = new PropertyMetadata(30.0d, propZoomOutThresholdChanged);
			ZoomOutThresholdProperty = DependencyProperty.Register(
				"ZoomOutThreshold",
				typeof(double),
				typeof(VSPChartView),
				propZoomOutThreshold);

			PropertyChangedCallback propZoomInThresholdChanged = new PropertyChangedCallback(OnZoomInThresholdChanged);
			PropertyMetadata propZoomInThreshold = new PropertyMetadata(1.0d, propZoomInThresholdChanged);
			ZoomInThresholdProperty = DependencyProperty.Register(
				"ZoomInThreshold",
				typeof(double),
				typeof(VSPChartView),
				propZoomInThreshold);

			PropertyChangedCallback propEnabledChanged = new PropertyChangedCallback(OnEnabledChanged);
			PropertyMetadata propEnabled = new PropertyMetadata(true, propEnabledChanged);
			EnabledProperty = DependencyProperty.Register(
				"Enabled",
				typeof(bool),
				typeof(VSPChartView),
				propEnabled);

			DefaultStyleKeyProperty.OverrideMetadata(
				typeof(VSPChartView), 
				new FrameworkPropertyMetadata(typeof(VSPChartView)));
		}
		#endregion

		#region Methods

		public override void OnApplyTemplate()
		{
			base.OnApplyTemplate();

			Viewport3D viewport = GetTemplateChild("Part_viewportMain") as Viewport3D;
			viewport.MouseDown  += new MouseButtonEventHandler(Viewport_MouseDown);
			viewport.MouseUp    += new MouseButtonEventHandler(Viewport_MouseUp);
			viewport.MouseWheel += new MouseWheelEventHandler(Viewport_MouseWheel);
			viewport.MouseMove  += new MouseEventHandler(Viewport_MouseMove);

			Button button = GetTemplateChild("Part_buttonFlip") as Button;
			button.Click += new RoutedEventHandler(FlipButton_Click);

			SetUIEnabledState();
		}

		/// <summary>
		/// Raises a ChartZoom event, which indicates the user has requested a smaller or larger FOV chart.
		/// </summary>
		/// <param name="dir">The direction to zoom</param>
		protected virtual void OnChartZoom(ChartZoomRoutedEventArgs.Direction dir)
		{
			ChartZoomRoutedEventArgs args = new ChartZoomRoutedEventArgs(PreviewChartZoomEvent, CurrentChart, dir);
			RaiseEvent(args);

			if (!args.Handled)
			{
				//TODO: Do stuff before 

				try
				{
					args = new ChartZoomRoutedEventArgs(ChartZoomEvent, CurrentChart, dir);
					RaiseEvent(args);
				}
				catch (Exception err)
				{
					string msg = err.ToString();
				}

				//TODO: Do stuff after
			}
		}

		private static void OnCurrentChartChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
		{
			VSPChartView view = target as VSPChartView;
			if (view != null)
			{
				view.DisplayNewChart();
				view.PerformCameraSet(20);
			}
		}

		private static void OnZoomFactorChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
		{
		}

		private static void OnZoomOutThresholdChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
		{
		}

		private static void OnZoomInThresholdChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
		{
		}

		private static void OnEnabledChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
		{
			VSPChartView view = target as VSPChartView;
			if (view != null)
			{
				view.SetUIEnabledState();
			}
		}

		private void DisplayNewChart()
		{
			if (this.CurrentChart == null || this.CurrentChart.ChartImage == null)
			{
				return;
			}

			ImageBrush frontBrush = this.Template.FindName("Part_imageBrushFront", this) as ImageBrush;
			ImageBrush backBrush  = this.Template.FindName("Part_imageBrushBack",  this) as ImageBrush;
			if (frontBrush != null && backBrush != null)
			{
				using (MemoryStream stream = new MemoryStream())
				{
					this.CurrentChart.ChartImage.Save(stream, Imaging.ImageFormat.Png);
					PngBitmapDecoder decoder = new PngBitmapDecoder(
						stream, 
						BitmapCreateOptions.PreservePixelFormat, 
						BitmapCacheOption.Default);

					frontBrush.ImageSource = decoder.Frames[0];
					backBrush.ImageSource  = decoder.Frames[0];
				}
			}
		}

		private void SetUIEnabledState()
		{
			if (this.Template != null)
			{
				BlurBitmapEffect blurEffect = this.Template.FindName("Part_blurEffectEnabledState", this) as BlurBitmapEffect;
				if (blurEffect != null)
				{
					RegisterName("Part_blurEffectEnabledState", blurEffect);

					DoubleAnimation animation = new DoubleAnimation();
					animation.To       = this.Enabled ? 0 : 10;
					animation.Duration = new Duration(TimeSpan.FromMilliseconds(500));

					Storyboard.SetTargetName(animation, "Part_blurEffectEnabledState");
					Storyboard.SetTargetProperty(animation, new PropertyPath(BlurBitmapEffect.RadiusProperty));

					Storyboard board = new Storyboard();
					board.Children.Add(animation);

					board.Begin(this);
				}
			}
		}

		private void PerformRotateAction(Point newPoint)
		{
			//First get the points that we will need
			Viewport3D        viewport = this.Template.FindName("Part_viewportMain", this) as Viewport3D;
			PerspectiveCamera camera   = this.Template.FindName("Part_cameraMain", this)   as PerspectiveCamera;

			//Is the drag start point [0,0]?  Then this is the very beginning of a drag.
			//Set and skip
			if (this.startDragPoint.X == 0 && this.startDragPoint.Y == 0)
			{
				this.startDragPoint = newPoint;
				return;
			}

			//Here we will convert our two points from normal point locations with the origin
			//in the top left corner to the origin in the middle of the page
			Point[] points = new Point[2];
			points[0] = this.startDragPoint;
			points[1] = newPoint;

			double halfWidth  = viewport.ActualWidth  / 2.0d;
			double halfHeight = viewport.ActualHeight / 2.0d;

			for (int i = 0; i < 2; i++)
			{
				points[i].X -= halfWidth;
				points[i].Y -= halfHeight;
				points[i].Y *= -1;
			}

			//And finally, we'll convert them to Polar.
			PolarCoordinate pol1 = new PolarCoordinate(points[0]);
			PolarCoordinate pol2 = new PolarCoordinate(points[1]);

			//What is the difference between the two points?
			double deltaTheta = pol2.Theta - pol1.Theta;
			if (deltaTheta == 0.0d)
			{
				//If there's no difference, then no need to rotate.
				return;
			}

			//Get the "up" direction of the camera, convert the vector on the X/Y plane to
			//a Polar coordinate, and apply the above difference.
			Vector3D oldUpDir = camera.UpDirection;
			PolarCoordinate polarUpDir = new PolarCoordinate(new Point(oldUpDir.X, oldUpDir.Y));
			polarUpDir.Theta -= deltaTheta;

			//Now, convert the new Polar coordinate BACK to a Cartesean point...
			Point newUpPoint = polarUpDir.ToPoint();

			//...and use that as a vector in the x/y plane to reorient the camera.
			//Then, normalize the vector.
			Vector3D newUpDir = new Vector3D(newUpPoint.X, newUpPoint.Y, 0);
			newUpDir.Normalize();

			//Set the camera to the new Up direction (which rotates it 'round the Z axis, mimicing
			//rotation of the page)
			camera.UpDirection = newUpDir;

			//And set the drag to the current point, which preps for the next drag.
			this.startDragPoint = newPoint;
		}

		private void PerformZoomAction(double delta)
		{
			PerspectiveCamera camera = this.Template.FindName("Part_cameraMain", this) as PerspectiveCamera;
			if (camera != null)
			{
				Point3D oldPos = camera.Position;
				double  newZ   = oldPos.Z - (delta * this.ZoomFactor);
				if (newZ < this.ZoomInThreshold)
				{
					newZ = this.ZoomInThreshold;
				}
				else if (newZ > this.ZoomOutThreshold)
				{
					newZ = this.ZoomOutThreshold;
				}

				if (oldPos.Z == newZ)
				{
					return;
				}

				if (newZ == this.ZoomOutThreshold)
				{
					OnChartZoom(ChartZoomRoutedEventArgs.Direction.Out);
				}
				else if (newZ == this.ZoomInThreshold)
				{
					OnChartZoom(ChartZoomRoutedEventArgs.Direction.In);
				}
			
				PerformCameraSet(newZ);
			}
		}

		private void PerformCameraSet(double zLoc)
		{
			PerspectiveCamera camera = this.Template.FindName("Part_cameraMain", this) as PerspectiveCamera;
			if (camera != null)
			{
				camera.Position = new Point3D(0, 0, zLoc);
			}
		}

		private void PerformChartFlip()
		{
			GeometryModel3D model = this.Template.FindName("Part_geometryMainPage", this) as GeometryModel3D;
			if (model != null)
			{
				RegisterName("Part_geometryMainPage", model);

				double angle = (this.isShowingFront) ? 180 : 0;

				Rotation3DAnimation animation = new Rotation3DAnimation();
				animation.To       = new AxisAngleRotation3D(new Vector3D(0,1,0), angle);
				animation.Duration = new Duration(TimeSpan.FromMilliseconds(500));
				
				Storyboard.SetTargetName(animation, "Part_geometryMainPage");
				Storyboard.SetTargetProperty(animation, new PropertyPath("(Model3D.Transform).(Transform3DGroup.Children)[0].(RotateTransform3D.Rotation)"));
			
				Storyboard board = new Storyboard();
				board.Children.Add(animation);

				board.Begin(this);
				this.isShowingFront = !this.isShowingFront;
			}
		}
		#endregion

		#region Event Handlers and Callbacks
		private void Viewport_MouseDown(object sender, MouseButtonEventArgs e)
		{
			if (!this.Enabled)
			{
				return;
			}

			if (e.ChangedButton == MouseButton.Right)
			{
				Viewport3D viewport = e.Source as Viewport3D;
				if (viewport != null)
				{
					this.startDragPoint = e.GetPosition(viewport);
				}
			}
		}

		private void Viewport_MouseUp(object sender, MouseButtonEventArgs e)
		{
			if (!this.Enabled)
			{
				return;
			}

			if (e.ChangedButton == MouseButton.Right)
			{
				this.startDragPoint = new Point(0,0);
			}
		}

		private void Viewport_MouseWheel(object sender, MouseWheelEventArgs e)
		{
			if (!this.Enabled)
			{
				return;
			}

			PerformZoomAction(e.Delta);
		}

		private void Viewport_MouseMove(object sender, MouseEventArgs e)
		{
			if (!this.Enabled)
			{
				return;
			}

			if (e.RightButton == MouseButtonState.Pressed)
			{
				Viewport3D viewport = e.Source as Viewport3D;
				if (viewport != null)
				{
					PerformRotateAction(e.GetPosition(viewport));
				}
			}
		}

		private void FlipButton_Click(object sender, RoutedEventArgs e)
		{
			if (!this.Enabled)
			{
				return;
			}

			PerformChartFlip();
		}
		#endregion
	}

	public delegate void ChartZoomRoutedEventHandler(object sender, ChartZoomRoutedEventArgs e);
	public class ChartZoomRoutedEventArgs : RoutedEventArgs
	{
		#region Enums
		public enum Direction
		{
			In, Out
		}
		#endregion

		#region Members
		private VSPChartDetail currentChart;
		private Direction      zoomDirection;
		#endregion

		#region Properties
		/// <summary>
		/// Gets the current chart properties
		/// </summary>
		public VSPChartDetail CurrentChart
		{
			get
			{
				return this.currentChart;
			}
		}

		/// <summary>
		/// Gets the direction the zoom action is going.
		/// </summary>
		public Direction ZoomDirection
		{
			get
			{
				return this.zoomDirection;
			}
		}

		#endregion

		#region C'Tors and object lifetime code
		public ChartZoomRoutedEventArgs(RoutedEvent routedEvent, VSPChartDetail currentChart, Direction dir) :
			this(routedEvent)
		{
			this.currentChart  = currentChart;
			this.zoomDirection = dir;
		}

		public ChartZoomRoutedEventArgs(RoutedEvent routedEvent) :
			base(routedEvent)
		{

		}

		protected ChartZoomRoutedEventArgs()
		{
		}
		#endregion
	}
}
